#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#include "fmap.h"


/*
 * todo: make fmap do IOs on a different thread ?
 */

static long fmap_page_size = 0;

FMAP_STATIC int fmap_init(fmap_t * self, int fd, int flags, size_t map_size, size_t map_ahead_size)
{
	if (fmap_page_size == 0) {
		fmap_page_size = sysconf(_SC_PAGE_SIZE);
	}
	if (map_size % fmap_page_size != 0) {
		// round up to page boundary
		map_size = ((map_size / fmap_page_size) + 1) * fmap_page_size;
	}
	// map_ahead_size must not exceed map_size
	RETURN_WITH_CODE_IF(FMAP_ERR_INVALID_SIZE, map_ahead_size > map_size);
	self->fd = fd;
	self->map_size = map_size;
	self->map_ahead_size = map_ahead_size;
	self->map_offset = 0;
	self->addr = NULL;
	self->prot = ((flags & FMAP_READ) ? PROT_READ : 0) |
	              ((flags & FMAP_WRITE) ? PROT_WRITE : 0);
	self->flags = MAP_SHARED |
	              ((flags & FMAP_READ_AHEAD) ? MAP_POPULATE : 0) |
	              ((flags & FMAP_LOCKED) ? MAP_LOCKED : 0) |
	              ((flags & FMAP_NOSWAP) ? MAP_NORESERVE : 0);
	return 0;
}

FMAP_STATIC int fmap_fini(fmap_t * self)
{
	self->fd = -1;
	if (self->addr != NULL) {
		munmap(self->addr, self->map_size);
		self->addr = NULL;
	}
	return 0;
}

FMAP_STATIC inline int _fmap_ensure_file_capacity(int fd, off_t length)
{
	struct stat sb;
	if (fstat(fd, &sb) != 0) {
		RETURN_WITH_CODE(FMAP_ERR_STAT_FAILED);
	}
	if (sb.st_size >= length) {
		return 0; // no op
	}
	if (ftruncate(fd, length) != 0) {
		RETURN_WITH_CODE(FMAP_ERR_TRUNCATE_FAILED);
	}
	/*
	char buf[1] = {0};
	if (lseek(fd, size - sizeof(buf), SEEK_SET) == -1) {
		RETURN_WITH_CODE(FMAP_ERR_LSEEK_FAILED);
	}
	if (write(fd, buf, sizeof(buf)) != sizeof(buf)) {
		RETURN_WITH_CODE(FMAP_ERR_WRITE_FAILED);
	}*/
	return 0;
}

FMAP_STATIC inline void * fmap_map(fmap_t * self, off_t offset, size_t size)
{
	off_t abs_offset, page_offset, end_offset, back_size, fwd_size;
	void * addr;

	// mapping with size = 0 only commits the memory region

	// cannot map more than map_size
	RETURN_WITH_CODE_IF2(FMAP_ERR_MAP_TOO_BIG, size > self->map_size, NULL);

	if (self->addr != NULL && (offset < self->map_offset ||
			offset + size > self->map_offset + self->map_size)) {
		// the requested offset and size are out of the range of the
		// current map
		munmap(self->addr, self->map_size);
		self->addr = NULL;
	}

	if (self->addr == NULL) {
		fwd_size = size + self->map_ahead_size;
		if (fwd_size > self->map_size) {
			fwd_size = self->map_size; // map_ahead_size is only a hint
		}
		back_size = self->map_size - fwd_size;
		abs_offset = (offset > back_size) ? offset - back_size : 0;
		page_offset = (abs_offset / fmap_page_size) * fmap_page_size;
		end_offset = page_offset  + self->map_size;
		// alignment problems -- shouldn't happen since the map_size is rounded up
		RETURN_WITH_CODE_IF2(FMAP_ERR_ALIGNMENT_ERROR, offset + size > end_offset, NULL);
		if (_fmap_ensure_file_capacity(self->fd, end_offset) != 0) {
			return NULL; // not enough disk space
		}
		addr = mmap(NULL, self->map_size, self->prot, self->flags, self->fd,
				page_offset);
		RETURN_WITH_CODE_IF2(FMAP_ERR_MMAP_FAILED, addr == MAP_FAILED, NULL);
		self->map_offset = page_offset;
		self->addr = addr;
	}

	return self->addr + (offset - self->map_offset);
}

/*
 * sliding window over fmap
 */
FMAP_STATIC int fwindow_init(fwindow_t * self, int fd, size_t map_size)
{
	self->pos = 0;
	return fmap_init(&self->map, fd, FMAP_WRITE, map_size, map_size);
}

FMAP_STATIC int fwindow_fini(fwindow_t * self)
{
	return fmap_fini(&self->map);
}

FMAP_STATIC inline int fwindow_write(fwindow_t * self, const void * buf, size_t size)
{
	char *addr = fmap_map(&self->map, self->pos, size);
	if (addr == NULL) {
		return -1;
	}
	if (buf != NULL) {
		memcpy(addr, buf, size);
	}
	self->pos += size;
	return 0;
}

FMAP_STATIC inline off_t fwindow_tell(fwindow_t * self)
{
	return self->pos;
}

FMAP_STATIC inline void fwindow_advance(fwindow_t * self, off_t delta)
{
	self->pos += delta;
}




